package com.unimod.fsml.model;

import static com.unimod.fsml.FSMLTokens.*;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposal;

import com.evelopers.common.exception.CommonException;
import com.evelopers.unimod.parser.InterpreterException;
import com.evelopers.unimod.plugin.eclipse.Helper;
import com.evelopers.unimod.runtime.ControlledObject;
import com.evelopers.unimod.runtime.EventProcessorException;
import com.evelopers.unimod.runtime.EventProvider;
import com.unimod.fsml.FSMLTokens.EVENT_TYPE;
import com.unimod.fsml.FSMLTokens.STATE_TYPE;
import com.unimod.fsml.model.lexer.FSMLLexer;
import com.unimod.fsml.model.lexer.Token;
import com.unimod.fsml.model.transform.FSMLToModel;
import com.unimod.fsml.util.CoreUtils;

/**
 * @author Ivan Lagunov
 */
public class FSMLAutoCompletion {
    private static final Log log = LogFactory.getLog(FSMLAutoCompletion.class);

	private FSMLModelHelper modelHelper;
	private boolean detailed = false;

	public FSMLAutoCompletion() {
		this.modelHelper = FSMLModelHelper.getInstance();
	}

	/**
	 * Switch off images and description in proposals. Actually such feature is
	 * needed for test cases, because they are ran not under eclipse workbench
	 * and has no access to eclipse runtime.
	 * 
	 * @param detailed
	 */
	public void setNeedDetailedProposal(boolean detailed) {
		this.detailed = detailed;
	}

	/**
	 * Returns list of {@link ICompletionProposal proposals} that may complete
	 * the expression
	 * 
	 * @param expression
	 *            expression to autocomplete
	 * @param offset
	 * @return list of {@link ICompletionProposal proposals} that may complete
	 *         the expression
	 * @throws com.evelopers.common.exception.CommonException
	 *             if Common exception occurs
	 */
	public ICompletionProposal[] autoComplete(String expression, int offset)
			throws CommonException {
		modelHelper.prepareAutoCompletion(expression, offset);

		return getCompletionProposals(offset, modelHelper.getCOMap());
	}

	/**
	 * @param offset
	 *            cursor offset in expression to autocomplete from
	 * @param map
	 * @return list of {@link ICompletionProposal proposals} that may complete
	 *         the expression
	 * @throws EventProcessorException
	 * @throws InterpreterException 
	 */
	private ICompletionProposal[] getCompletionProposals(int offset,
			ParserCOMap map) throws EventProcessorException, InterpreterException {
		FSMLLexer lexer = map.getLexer();
		Stack stack = map.getStack();
		FSMLToModel fsmlToModel = map.getFsmlToModel();
		ObjectContext objectContext = map.getObjectContext();

		Token prevToken = lexer.getPrevToken();
		int prevTokenType = (prevToken != null) ? prevToken.getTypeId() : E1;
		Token nextToken = lexer.getNextToken();
		String exprRest = (nextToken != null) ? nextToken.getValue().substring(
				0, Math.max(0, offset - nextToken.getStart())) : "";

		Set<Integer> consumableTokens = lexer.getConsumableTokens();

		List<ICompletionProposal> cpList = new LinkedList<ICompletionProposal>();

		/*
		 * Lexer may finish in one the three states:
		 * 
		 * 1. exprRest == "" - it means that last character before offset is
		 * blank. we CAN'T autocomplete last ident, if it were, but we can
		 * autocomplete all outgoing transitions from current active state of
		 * syntax analyzer
		 * 
		 * 2. (exprRest != "") && last token is known - it means that we can
		 * autocomplete last ident (it it were actually ident) and all outgoing
		 * transitions from current active state of syntax analyzer
		 */
		for (Integer nextTokenType : consumableTokens) {
			cpList
					.addAll(getCompletionProposalsForTokenType(offset,
							prevTokenType, nextTokenType, exprRest,
							stack, objectContext, fsmlToModel));
		}
		modelHelper.finishAutoCompletion();
		
		return cpList.toArray(new ICompletionProposal[cpList.size()]);
	}

	/**
	 * Returns the list of completion proposals for given fsml token type.
	 * 
	 * @param offset
	 *            the offset of token.
	 * @param prevType
	 *            the type of previous token.
	 * @param nextType
	 *            the type of token.
	 * @param tokenRest
	 *            the value of token.
	 * @param stack
	 *            the stack controlled object.
	 * @param oc
	 *            the object context controlled object.
	 * @param fsmlToModel
	 *            the fsml to model converter controlled object.
	 * @return list of proposals that may complete token.
	 */
	private List<ICompletionProposal> getCompletionProposalsForTokenType(
			int offset, int prevType, int nextType, String tokenRest,
			Stack stack, ObjectContext oc, FSMLToModel fsmlToModel) {
		List<ICompletionProposal> values = new LinkedList<ICompletionProposal>();

		switch (nextType) {
		case E2:
			for (int i = 0; i < TYPES.length; i++) {
				addSimpleTextElement(values, offset, TYPES[i], tokenRest);
			}
			break;
		case E3:
			for (int i = 0; i < BOOL_CONSTS.length; i++) {
				addSimpleTextElement(values, offset, BOOL_CONSTS[i], tokenRest);
			}
		case E4:
			// Integer const.
			break;
		case E5:
			// Identifier.
			values = getCompletionProposalsForIdent(offset, oc, fsmlToModel, prevType, tokenRest);
			break;
		case E6:
			addSimpleTextElement(values, offset, AND, tokenRest);
			break;
		case E7:
			addSimpleTextElement(values, offset, OR, tokenRest);
			break;
		case E8:
			for (int i = 0; i < RELATIONS.length; i++) {
				addSimpleTextElement(values, offset, RELATIONS[i], tokenRest);
			}
			break;
		case E9:
			// EOF.
			break;
		case E10:
			// Dot follows immediately after identifier, should be added automatically.
			break;
		case E11:
			addSimpleTextElement(values, offset, NOT, tokenRest);
			break;
		case E12:
			addSimpleTextElement(values, offset, COMMA, tokenRest);
			break;
		case E13:
			addSimpleTextElement(values, offset, SEMICOLON, tokenRest);
			break;
		case E14:
			addSimpleTextElement(values, offset, OPEN_BRACE, tokenRest);
			break;
		case E15:
			// If brace is on top of the stack.
			if (stack.x3(null)) {
				addSimpleTextElement(values, offset, CLOSE_BRACE, tokenRest);
			}
			break;
		case E16:
			addSimpleTextElement(values, offset, OPEN_PARENTHESIS, tokenRest);
			break;
		case E17:
			// If parenthesis is on top of the stack.
			if (stack.x2(null)) {
				addSimpleTextElement(values, offset, CLOSE_PARENTHESIS,
						tokenRest);
			}
			break;
		case E18:
			addSimpleTextElement(values, offset, STATE_TYPE.INITIAL, tokenRest);
			break;
		case E19:
			addSimpleTextElement(values, offset, STATE_TYPE.FINAL, tokenRest);
			break;
		case E20:
			addSimpleTextElement(values, offset, EXECUTE, tokenRest);
			break;
		case E21:
			// If not final state
			if (!fsmlToModel.x2(null)) {
				addSimpleTextElement(values, offset, TRANSITTO, tokenRest);
			}
			break;
		case E22:
			// If not initial state
			if (!fsmlToModel.x1(null)) {
				addSimpleTextElement(values, offset, ON, tokenRest);
			}
			break;
		case E23:
			addSimpleTextElement(values, offset, STATEMACHINE, tokenRest);
			break;
		case E24:
			addSimpleTextElement(values, offset, IF, tokenRest);
			break;
		case E25:
			addSimpleTextElement(values, offset, ELSE, tokenRest);
			break;
		case E26:
			addSimpleTextElement(values, offset, EVENT_TYPE.ENTER, tokenRest);
			break;
		case E27:
			addSimpleTextElement(values, offset, EVENT_TYPE.ANY, tokenRest);
			break;
		case E28:
			addSimpleTextElement(values, offset, EVENT_TYPE.EXIT, tokenRest);
			break;
		case E29:
			addSimpleTextElement(values, offset, USES, tokenRest);
			break;
		case E30:
			addSimpleTextElement(values, offset, INCLUDE, tokenRest);
			break;
		default:
			break;
		}

		return values;
	}

	/**
	 * Adds completion proposal for textual token.
	 * 
	 * @param values
	 *            the list of completion proposals.
	 * @param offset
	 *            the offset for completion.
	 * @param item
	 *            the value to complete to.
	 * @param tokenRest
	 *            the initial part to complete.
	 */
	private void addSimpleTextElement(List<ICompletionProposal> values,
			int offset, String item, String tokenRest) {
		if (item.startsWith(tokenRest) && (tokenRest.length() != item.length())) {
			addProposal(values, offset, item, tokenRest, item);
		}
	}

	/**
	 * Adds completion proposal for textual token with different item and
	 * display strings.
	 * 
	 * @param values
	 *            the list of completion proposals.
	 * @param offset
	 *            the offset for completion.
	 * @param item
	 *            the value to complete to.
	 * @param tokenRest
	 *            the initial part to complete.
	 * @param displayString
	 *            the string to show on display.
	 */
	private void addComplexTextElement(List<ICompletionProposal> values,
			int offset, String item, String tokenRest, String displayString) {
		if (item.startsWith(tokenRest) && (tokenRest.length() != item.length())) {
			addProposal(values, offset, item, tokenRest, displayString);
		}
	}

	private void addProposal(List<ICompletionProposal> filteredList,
			int offset, String item, String prefix, String displayString) {
		filteredList
				.add(new CompletionProposal(item, offset - prefix.length(),
						prefix.length(), item.length(), null, displayString,
						null, null));
	}

	private List<ICompletionProposal> getCompletionProposalsForIdent(
			int offset, ObjectContext context, FSMLToModel fsmlToModel, int prevType, String tokenRest) {
		switch (context.getContextType()) {
		case GUARD:
			return getControlledObjectsProposals(offset, context, fsmlToModel, tokenRest, false);
		case ACTION:
			return getControlledObjectsProposals(offset, context, fsmlToModel, tokenRest, true);
		case EVENT:
			return getEventProposals(offset, fsmlToModel, tokenRest);
		case EVENT_PROVIDER:
			return getClassNameProposals(offset, context, prevType, tokenRest, true);
		case CONTROLLED_OBJECT:
			return getClassNameProposals(offset, context, prevType, tokenRest, false);
		case STATE:
			return getStateProposals(offset, fsmlToModel, tokenRest);
		}
		return Collections.emptyList();
	}

	private List<ICompletionProposal> getClassNameProposals(int offset, ObjectContext context, int prevType, String tokenRest, boolean isEventProvider) {
		List<ICompletionProposal> cpList = new LinkedList<ICompletionProposal>();

		// Previous token can't be identifier.
		// It may be semicolon, dot or keyword 'uses'.
		if (prevType != E5) {
			String packageName = context.peekFullJavaPath();
			try {
				IPackageFragment packageFragment = Helper.findJavaPackage(packageName);
				for (ICompilationUnit element : packageFragment.getCompilationUnits()) {
					Set<String> interfaces = CoreUtils.getInterfaces(element);
					if ((isEventProvider && interfaces.contains(
							CoreUtils.createClassSignature(EventProvider.class))) ||
					   (!isEventProvider && interfaces.contains(
							CoreUtils.createClassSignature(ControlledObject.class)))) {
						String[] nameParts = element.getElementName().split("\\.", 2);
						addSimpleTextElement(cpList, offset, nameParts[0], tokenRest);
					}
				}
				IJavaElement[] packages = ((IPackageFragmentRoot)packageFragment.getParent()).getChildren();
				int packagePartsNumber = context.getStackDepth();
				for (IJavaElement childPackage : packages) {
					String childPackageName = childPackage.getElementName();
					if (childPackageName.length() != 0) {
						String[] childPackageParts = childPackageName.split("\\.");
						if (childPackageParts.length == packagePartsNumber + 1) {
							String lastPackagePart = childPackageParts[childPackageParts.length - 1];
							addComplexTextElement(cpList, offset, lastPackagePart + DOT, tokenRest, lastPackagePart);
						}
					}
				}
			} catch (JavaModelException e) {
				log.warn(String.format("Cannot load specified package: %s", packageName), e);
			}			
		}
		
		return cpList;
	}

	private List<ICompletionProposal> getStateProposals(int offset, FSMLToModel fsmlToModel, String tokenRest) {
		List<ICompletionProposal> cpList = new LinkedList<ICompletionProposal>();

		for (Iterator<String> stIterator = fsmlToModel.getStates(); stIterator.hasNext();) {
			String stateName = stIterator.next();
			
			addSimpleTextElement(cpList, offset, stateName, tokenRest);
		}
		
		return cpList;
	}

	private List<ICompletionProposal> getEventProposals(int offset, FSMLToModel fsmlToModel, String tokenRest) {
		List<ICompletionProposal> cpList = new LinkedList<ICompletionProposal>();

		for (Iterator<String> epIterator = fsmlToModel.getEventProvidersIterator(); epIterator.hasNext();) {
			String epClass = epIterator.next();

			try {
				IType epType = Helper.findJavaType(epClass);
				IField[] epFields = Helper.getEventsWithDerrived(epType);

				for (int i = 0; i < epFields.length; i++) {
					Object constant = epFields[i].getConstant();
					if (constant instanceof String) {
						String eventName = (String) constant;
						addSimpleTextElement(cpList, offset, 
								eventName.substring(1, eventName.length() - 1), tokenRest);
					}
				}

			} catch (JavaModelException e) {
				// TODO: Highlight erroneous type as it wasn't found.
			}
		}

		return cpList;
	}

	private List<ICompletionProposal> getControlledObjectsProposals(int offset,
			ObjectContext context, FSMLToModel fsmlToModel, String tokenRest, boolean isOutputAction) {
		List<ICompletionProposal> cpList = new LinkedList<ICompletionProposal>();

		if (context.isIdTokenFinished()) {
			String coName = context.peekTopElement();
			String coClass = fsmlToModel.getControlledObjectClass(coName);

			try {
				IType coType = Helper.findJavaType(coClass);
				IMethod[] coMethods = Helper.getActionsWithDerrived(coType);

				for (int i = 0; i < coMethods.length; i++) {
					if (isOutputAction == coMethods[i].getReturnType().equals(Signature.SIG_VOID)) {
						String methodName = coMethods[i].getElementName();
						addSimpleTextElement(cpList, offset, methodName, tokenRest);
					}
				}

			} catch (JavaModelException e) {
				// TODO: Highlight erroneous type as it wasn't found.
			}
		} else {
			for (Iterator<String> coIterator = fsmlToModel.getControlledObjectsIterator(); coIterator.hasNext();) {
				String coName = coIterator.next();
				addComplexTextElement(cpList, offset, coName + DOT, tokenRest, coName);
			}
		}

		return cpList;
	}
}
